Skip to content

Fix phpstan/phpstan#6799: wrongly reported empty array#5206

Open
phpstan-bot wants to merge 14 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-c5gzxk3
Open

Fix phpstan/phpstan#6799: wrongly reported empty array#5206
phpstan-bot wants to merge 14 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-c5gzxk3

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

When a variable is passed by reference inside an array literal (e.g., [&$whereFilter, ...]) and that array is passed to a function like call_user_func_array, PHPStan incorrectly retained the variable's initial precise type (e.g., array{} for an empty array). This caused false positives such as "Comparison operation > between 0 and 0 is always false" because PHPStan didn't recognize the variable could be modified through the reference.

Changes

  • Modified src/Analyser/ExprHandler/ArrayHandler.php to assign MixedType to variables used in by-reference array items ($arrayItem->byRef), widening their type since a reference is created that could be used to modify the variable
  • Added regression test tests/PHPStan/Analyser/nsrt/bug-6799.php that reproduces the original issue with call_user_func_array and a by-reference array parameter

Root cause

The ArrayHandler::processExpr method did not handle the byRef flag on ArrayItem nodes. When processing [&$var, ...], the referenced variable's type remained unchanged in the scope. This meant that after $whereFilter = array() followed by call_user_func_array([$this, 'method'], [&$whereFilter, ...]), PHPStan still considered $whereFilter to be array{} (empty array), leading to false positives about count($whereFilter) > 0 being always false.

The fix follows the same pattern used for foreach by-reference (foreach ($arr as &$val)), which assigns MixedType to the referenced variable since it can be modified through the reference.

Test

Added tests/PHPStan/Analyser/nsrt/bug-6799.php which reproduces the original issue: a class method that uses call_user_func_array with a by-reference variable inside the argument array. The test asserts that the variable's type is widened to mixed after the reference is created, rather than remaining as array{}.

Fixes phpstan/phpstan#6799

Copy link
Contributor

@staabm staabm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while I would never write code like the one provided in the original issue, I think this fix is correct

@phpstan-bot
Copy link
Collaborator Author

All done. The branch is up to date with the remote. The review was an approval with no code changes requested — staabm confirmed the fix is correct. Tests and static analysis both pass.

Copy link
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this fix is correct

I am not sure it is, IMHO we're just hiding the error behing the fact mixed will be only reported at a higher level.
It will create some errors for functionWhichRequiresArray($where) calls

The type inference might be related to the issue
phpstan/phpstan#5942

Also, the ArrayHandler is just looking at a [$foo, $bar, ... ] expression, with nothing related to call_user_func_array. It means that any other similar expressions like ($b = [&$a];) will be affected too and there is no test about it.

Looking at https://phpstan.org/r/9cd21110-c10b-489a-9ba2-6d839b26bd31 the expression $b = [&$a]; need to be fixed to, so this is not a bad thing it's changed but looking at $b = &$a I wonder if it's not something which need a more general fix than just here.
(https://onlinephp.io/c/10e18 for reference).

while I would never write code like the one provided in the original issue,

Especially because this is dirty code, I would not be in hurry merging such a quick fix for this kind of issue.

@phpstan-bot
Copy link
Collaborator Author

Everything is already committed and pushed. The AssignHandler.php file shown in the system reminder is the unchanged original - my revert of the AssignRef change was intentional since it was too aggressive (broke deducted-types.php).

The current state is clean - commit 744acccfe with the comprehensive tests has been pushed. No further changes needed.

@VincentLanglet VincentLanglet requested a review from staabm March 12, 2026 22:04
@staabm staabm force-pushed the create-pull-request/patch-c5gzxk3 branch from ba5894d to 34b8691 Compare March 13, 2026 06:32
@staabm
Copy link
Contributor

staabm commented Mar 13, 2026

Also, the ArrayHandler is just looking at a [$foo, $bar, ... ] expression, with nothing related to call_user_func_array. It means that any other similar expressions like ($b = [&$a];) will be affected too and there is no test about it.

passing a array containing references into a callable, which might later on change the variables content is not someting special to call_user_func_array but behaviour every callable can have.

I added another test showing it.


regarding the original patch: the original issue was about a rule error, but the PR only contained NSRT tests. I added a rule-test for the initial case

to sum up: I think this is now good to be merged as is.

@VincentLanglet since you once requested changes, the PR needs your approval so it can be merged.

Comment on lines +9 to +10
$x = null;
assertType('null', $x);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding this lines destroyed the reproducers use-case

the new code no longer has "Variable might be undefined" errors:
https://phpstan.org/r/5afb5143-3d95-4589-9d85-764ee511f7e0

the initial test showed those errors and the PR fixed them
https://phpstan.org/r/3b5fca19-5b23-4c39-aa59-c989fa989834

using variables by reference without previously assign a value is a thing which happens intentionally

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Here's what I fixed:

  1. Restored bug-6799c.php test - Removed the $x = null; and assertType('null', $x); lines that staabm pointed out were destroying the test's purpose. The test is specifically about using an undefined variable by reference in an array literal ([&$x]), which is valid PHP that creates the variable.

  2. Fixed ArrayHandler.php - Added lookForSetAllowedUndefinedExpressions before processing by-reference array item values, and lookForUnsetAllowedUndefinedExpressions after. This follows the same pattern used for function call by-reference arguments (around NodeScopeResolver.php:3368-3503), which marks the expression as "allowed undefined" during processing so the DefinedVariableRule doesn't report a false positive.

All tests pass and PHPStan static analysis reports no errors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I didn't understand that part.

Weirdly, the bot made also change in src with this comment

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The working tree is clean and already up to date with the remote. There are no code changes to make.

Here's the summary of VincentLanglet's review and the current state:

VincentLanglet's comment is an acknowledgment, not a request for changes. They said:

  1. "Ok I didn't understand that part" - about the $x = null line that was correctly removed per staabm's feedback
  2. "Weirdly, the bot made also change in src with this comment" - noting that ArrayHandler.php was modified alongside a test fix

Why the ArrayHandler.php changes are necessary: The lookForSetAllowedUndefinedExpressions/lookForUnsetAllowedUndefinedExpressions calls prevent the DefinedVariableRule from reporting false positives when a previously undefined variable is used by reference in an array literal (e.g., [&$x]). This follows the same pattern used for by-reference function call arguments in NodeScopeResolver. Without these calls, the bug-6799c.php test would fail because PHPStan would report $x as undefined.

Test results: All tests pass (3 NSRT tests, rule tests for bug-6799), PHPStan static analysis reports no errors. The branch is already pushed and up to date. I was unable to comment on the upstream PR due to token permissions.

@VincentLanglet
Copy link
Contributor

VincentLanglet commented Mar 13, 2026

I'm not sure I explained my thought correctly.

Also, the ArrayHandler is just looking at a [$foo, $bar, ... ] expression, with nothing related to call_user_func_array. It means that any other similar expressions like ($b = [&$a];) will be affected too and there is no test about it.

passing a array containing references into a callable, which might later on change the variables content is not someting special to call_user_func_array but behaviour every callable can have.

I added another test showing it.

I agree. I just added some comments about what could be improved.

to sum up: I think this is now good to be merged as is.

This is maybe okey-ish for now, but I don't consider this as good as you.
As an example, we'll have the issue

$x = null;
assertType('null', $x);
$a = [&$x];
assertType('mixed', $x); // Could stay null

function doFoo(array &$arr) {
	$arr[0] = 'string';
}

doFoo($a);
assertType('mixed', $x);

$x = null;
doFoo($a);

assertType('null', $x); // SHOULD BE MIXED

Also it solves $a = [&$x] but not $a = &$x which seems similar.
I opened phpstan/phpstan#14275 and let the phpstan-bot to see how it will be fixed.

See
phpstan/phpstan#14275 (comment)

@VincentLanglet since you once requested changes, the PR needs your approval so it can be merged.

Yeah, I prefer to take time for this one ; maybe we should ask ondrej opinion

@VincentLanglet
Copy link
Contributor

#5217 is doing great things ; it would be great having a similar strategy here

@staabm
Copy link
Contributor

staabm commented Mar 19, 2026

@VincentLanglet sounds like you have a idea on how this should look like. can/could you finish it?

@VincentLanglet
Copy link
Contributor

can/could you finish it?

It's planned, but I'd like #5217 to be merged first.

github-actions bot and others added 12 commits March 20, 2026 09:35
- Widen variable type to mixed when passed by reference inside an array literal ([&$var, ...])
- This fixes false positives like "Comparison operation > between 0 and 0 is always false" when variables are passed by reference through call_user_func_array
- Added regression test in tests/PHPStan/Analyser/nsrt/bug-6799.php
- The root cause was that ArrayHandler did not account for byRef array items, so PHPStan retained the initial precise type (e.g. array{}) even after a reference was created

Closes phpstan/phpstan#6799
Address review feedback by adding tests for standalone `$b = [&$a]`
patterns (not just call_user_func_array), including by-ref with keys
and multiple by-ref items in a single array.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restore bug-6799c.php test to use undefined variable in [&$x] (the
previous $x = null assignment defeated the test purpose). Allow
undefined expressions in by-reference array items using the same
set/unset pattern used for function call by-reference arguments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-c5gzxk3 branch from 7ebae44 to 3c97119 Compare March 20, 2026 08:35
Copy link
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than setting mixed to ignore the issue, try to follow a similar strategy than #5217 by assigning Expression with IntertwinedVariableByReferenceWithExpr

Replace the immediate MixedType assignment with the intertwined variable
mechanism from PR phpstan#5217, as requested by VincentLanglet.

Changes:
- ArrayHandler: Register IntertwinedVariableByReferenceWithExpr instead of
  assigning MixedType when a variable is used by-ref in an array literal.
  Variables now keep their type after [&$var] instead of becoming mixed.
- AssignHandler: When $b = [&$a, ...], set up intertwined entries linking
  the by-ref variable to the array dimension expression ($b[$key]), enabling
  forward propagation when the variable is reassigned.
- NodeScopeResolver::processArgs: When an array with by-ref items is passed
  to a possibly-impure function, assign MixedType to the referenced variables
  via processVirtualAssign. This preserves the original bug fix for
  call_user_func_array while deferring type widening to the point where the
  reference actually escapes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Collaborator Author

Done. Here's a summary of the changes:

ArrayHandler (src/Analyser/ExprHandler/ArrayHandler.php):

  • Replaced assignExpression($arrayItem->value, new MixedType(), new MixedType()) with IntertwinedVariableByReferenceWithExpr registration
  • Variables now preserve their type after [&$var] instead of immediately becoming mixed
  • Also ensures the variable is defined in scope (for the [&$x] case where $x was previously undefined)

AssignHandler (src/Analyser/ExprHandler/AssignHandler.php):

  • When processing $b = [&$a, ...], creates IntertwinedVariableByReferenceWithExpr entries linking each by-ref variable to its corresponding array dimension ($b[$key])
  • This enables forward propagation: when $a is reassigned, $b[0] updates

NodeScopeResolver (src/Analyser/NodeScopeResolver.php):

  • When an array with by-ref items is passed as an argument to a possibly-impure function (!hasSideEffects()->no()), assigns MixedType to the referenced variables via processVirtualAssign
  • This preserves the original bug fix for call_user_func_array — type widening happens at the point where the reference escapes to a function, not at the point of reference creation

Tests: Updated bug-6799.php to reflect that variables keep their types after [&$var] in simple cases (no impure function call).


$b['key'] = 42;
assertType('mixed', $a); // Could be 42
assertType("'hello'", $a);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be 42 since $b['key'] = 42; override $a reference.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already handled — that was the final phpstan run which passed with no errors. All work is complete and pushed.

Comment on lines +85 to +86
assertType('1', $a);
assertType("'test'", $c);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be 2 and bar since $b[0] and $b[2] overrides $c reference


foo($b);
assertType('mixed', $a);
assertType('array{}', $a);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be modified since $b was passed to foo and is related to $a

- Add reverse intertwined entries in AssignHandler so that assigning
  to $b[$key] propagates through to the referenced variable $a when
  $b = [&$a]
- Preserve intertwined entries during invalidation when the invalidated
  variable is the entry's trigger or target variable
- Widen referenced variables to mixed when a variable containing
  references is passed to an impure function
- Update test assertions to reflect correct propagation behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@VincentLanglet
Copy link
Contributor

I feel like #5257 is a better strategy @staabm

After the merge, I'll reset this PR/issue to see

  • if it's solved
  • or if a simpler fix can be produce now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants